import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.io.File;
import java.io.ObjectInputStream.GetField;
import java.sql.*;

import javax.sql.*;

import ncsa.hdf.object.h5.*;

public class KMeans2 {
	static DatabaseConnector dc = new DatabaseConnector();
	static Statement stmt;
	
	private static Random rand = new Random(123456789L);
	ArrayList<double[]> clustSum;
	
	private static double tempo;
	private static double key;
	private static double loudness;
	private static double hotness;
	
	private static String title;
	private static String songID;
	
//	private static ArrayList<Song2> songs;
	
	/* 
	 * 
	 * Assumptions:
	 * 1. The songs' properties already have their values normalized from 0 to 1.
	 * 2. Each song has 7 properties and can be accessed using the getProp method.
	 * 3. The songs' properties have all been normalized already.
	 * 
	 * Return the K-Means clusters for the songs.
	 */
	public static void main(String[] args) {
		ArrayList<Song2> songs = new ArrayList<Song2>();
		tempo = 0.0;
		key = 0.0;
		loudness = 0.0;
		hotness = 0.0;
		title = "";
		songID = "";
		
		songs = new ArrayList<Song2>();
		Statement stmt = dc.getStatement();
		try
    	{
			String sql;
			sql = "SELECT `Tempo`,`Key`,`Name`,`SongID`,`Loudness`,`Hotness` FROM `song_recommendation`.`Song`";
			ResultSet rs = stmt.executeQuery(sql);
			while (rs.next()) 
			{
				key = rs.getDouble("Key");
				tempo = rs.getDouble("Tempo");
				loudness = rs.getDouble("Loudness");
				hotness = rs.getDouble("Hotness");
				if (tempo == 0) tempo = rand.nextDouble();
				if (loudness == 0) loudness = rand.nextDouble();
				if (hotness == 0) hotness = rand.nextDouble();
				title = rs.getString("Name");
				songID = rs.getString("SongID");
				
				songs.add(new Song2(tempo, key, loudness, hotness, title, songID));
			}
			rs.close();
		} catch(SQLException se) {
			se.printStackTrace();
		}
		System.out.println("STARTING TO CLUSTER...");
		int k = 10;
		HashMap<Integer,ArrayList<Song2>> matches = new HashMap<Integer,ArrayList<Song2>>();
		try {
			matches = findKMeans(k,5,songs);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("CLUSTERING DONE!");
	}
	
	public static HashMap<Integer, ArrayList<Song2>> findKMeans(int k, int numIter, ArrayList<Song2> songs) throws Exception {
		
		double[] means = new double[k];
		means = kplusinit(k, songs); // use k-means++ to optimize cluster coefficients
		for (int i=0; i<means.length; i++) {
			System.out.println("mean "+i+": "+means[i]);
		}
		HashMap<Integer,ArrayList<Song2>> matches = new HashMap<Integer,ArrayList<Song2>>(); // song matches for each cluster
		ArrayList<double[]> clustSum = new ArrayList<double[]>(); // sum for each property within a cluster
		double[] propMatchSums;
		// initially assign sums of each property to be 0 for each cluster
		// assign random means initially between 0 and 1, assume there are 7 properties for songs
//		for (int i=0; i<k; i++) {
//			means[i] = rand.nextDouble();
//			System.out.println("mean "+i+": "+means[i]);
//		}
//		System.out.println();
		double closestDist;
		double distance;
		int closestCluster;
		Song2 closestSong;
		// find closestSong mean (from a cluster) to each of the songs and assign that song to that cluster
		//boolean change = true;
		for (int iter=0; iter<numIter; iter++) {
		//while (change)
			// clear
			propMatchSums = new double[]{0.0,0.0,0.0,0.0, 0};
			matches.clear();
			clustSum.clear();
			for (int i=0; i<k; i++) {
				matches.put(i, new ArrayList<Song2>()); // initialize empty matches for each cluster
				clustSum.add(i, propMatchSums);
			}
			
			closestDist = 100;
			distance = 0;
			closestCluster = 0;
			closestSong = null;
			
			// start iteration
			for (int n=0; n<songs.size(); n++) 
			{
				closestSong = songs.get(n);
				for (int p=0; p<k; p++) {
					// find euclidean distance: tempo, hotness, year, mode, key, key, loudness
					distance = Math.sqrt(Math.pow(closestSong.getProp(0)-means[p],2) + Math.pow(closestSong.getProp(1)-means[p],2) + Math.pow(closestSong.getProp(2)-means[p],2) +  Math.pow(closestSong.getProp(3)-means[p],2));
					if (distance<closestDist) {
						closestDist = distance;
						closestCluster = p;
					}
				}
//				System.out.println("song "+n+" in "+closestCluster+ " with distance "+closestDist);
				ArrayList<Song2> newClust = new ArrayList<Song2>();
				newClust = matches.remove(closestCluster);
				newClust.add(closestSong);
				matches.put(closestCluster, newClust); // add matched song to corresponding cluster
				// update the sum for corresponding cluster
				propMatchSums = new double[]{(clustSum.get(closestCluster)[0]+closestSong.getProp(0)), (clustSum.get(closestCluster)[1]+closestSong.getProp(1)), (clustSum.get(closestCluster)[2]+closestSong.getProp(2)), (clustSum.get(closestCluster)[3]+closestSong.getProp(3)), clustSum.get(closestCluster)[4]+1};
				clustSum.set(closestCluster, propMatchSums);
				//for (int i=0; i<7; i++) System.out.println(propMatchSums[i]);
				//System.out.println();
				distance = 100;
			}
			
			double sum;			
			for (int i=0; i<k; i++) {
				//oldMeans = means[i];

				// compute the sum of within the cluster
				sum = clustSum.get(i)[0] + clustSum.get(i)[1] + clustSum.get(i)[2] + clustSum.get(i)[3];
				
				// update the means
				double numMatch = clustSum.get(i)[4];
				//if (numMatch != 0) means[i] = sum/numMatch;
				if (numMatch != 0) means[i] = sum/numMatch/4; //System.out.println("old: "+oldMeans+", new: "+means[i]); }; //System.out.println(means[i]); System.out.println(sum); System.out.println(numMatch); System.out.println("cluster "+i); }
				else means[i] = rand.nextDouble();
				// if the clusters contain the same items and the corresponding means are unchanged, break out
			}
		}
		
		// get only the filled clusters
		HashMap<Integer,ArrayList<Song2>> fillClusters = new HashMap<Integer,ArrayList<Song2>>();
		int y = 0;
		for (int i=0; i<k; i++) {
			if (matches.get(i).size() > 0) {
				fillClusters.put(y, matches.get(i));
				y++;
			}
		}

		try
    	{
			System.out.println(y + " clusters.");
			for (int i=0; i<fillClusters.size(); i++) {
				System.out.println(i+"th cluster has " + fillClusters.get(i).size() + " songs.");
			}
			
			String sql;
			for (int j=0; j<fillClusters.size(); j++) {
				System.out.println("Filling cluster " + j + "...");
				for (int q=0; q<fillClusters.get(j).size(); q++) {
					sql = "UPDATE `song_recommendation`.`Song` SET `Cluster`= ? WHERE `SongID`= ?";
					PreparedStatement updateQuery = dc.getConnection().prepareStatement(sql);
					updateQuery.setInt(1, j);
					updateQuery.setString(2, fillClusters.get(j).get(q).getID());
					updateQuery.executeUpdate();
					//System.out.println(fillClusters.get(j).get(q).getTitle() + " is placed in cluster " + j);
					if (updateQuery != null) try { updateQuery.close(); } catch (SQLException ignore) {}
					if (q==Math.floor(fillClusters.get(j).size()/2)) System.out.println("Half-way done with cluster "+j+" ["+q+"]...");
				}
			}
		} catch(SQLException se) {
			se.printStackTrace();
		}
		
		// after all the iterations have completed, return the clusters with the filled songs
		return matches;
	}
	
	/* K-means++ initialization algorithm 
	 * http://en.wikipedia.org/wiki/K-means%2B%2B
	 */
	private static double[] kplusinit(int k, ArrayList<Song2> songs) throws Exception {
		Song2 randSong = songs.get(rand.nextInt(songs.size()));
		ArrayList<Double> distances = new ArrayList<Double>();
		double[] ks = new double[k];
		ks[0] = Math.sqrt(Math.pow(randSong.getProp(0),2) + Math.pow(randSong.getProp(1),2) + Math.pow(randSong.getProp(2),2));
		Song2 closestSong;
		double ranDist;
		for (int e=1; e<k; e++) {
			for (int i=0; i<songs.size(); i++) {
				closestSong = songs.get(i);
				distances.add(Math.sqrt(Math.pow(closestSong.getProp(0)-randSong.getProp(0),2) + Math.pow(closestSong.getProp(1)-randSong.getProp(1),2) + Math.pow(closestSong.getProp(2)-randSong.getProp(2),2)));
				//if (distances.get(i) != 0) distances.set(i, distances.get(i)+Math.pow(distances.get(i), 2));
			}
			ranDist = distances.get(rand.nextInt(songs.size()));
			randSong = songs.get(distances.indexOf(ranDist));
			ks[e] = ranDist;
		}
		return ks;
	}

	/*
	 * A getter method to retrieve the averages of each of the properties within each cluster, which can be used for analysis purposes.
	 */
	public ArrayList<double[]> getClustAnalysis(int k, ArrayList<double[]> matches) {
		for (int i=0; i<k; i++) {
			
		}
		return clustSum;
	}
}
